package cu.ds.logicalsync.fytasrealm.communication;

import java.io.IOException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;

import cu.ds.logicalsync.fytasrealm.communication.event.PeerStatusEvent;
import cu.ds.logicalsync.fytasrealm.communication.event.PeerStatusListener;
import cu.ds.logicalsync.fytasrealm.communication.model.PeerInfo;
import cu.ds.logicalsync.fytasrealm.model.World;
import cu.ds.logicalsync.fytasrealm.util.Tools;

import net.jxta.discovery.DiscoveryEvent;
import net.jxta.discovery.DiscoveryListener;
import net.jxta.discovery.DiscoveryService;
import net.jxta.document.Advertisement;
import net.jxta.document.AdvertisementFactory;
import net.jxta.peer.PeerID;
import net.jxta.protocol.DiscoveryResponseMsg;
import net.jxta.protocol.PeerAdvertisement;
import net.jxta.protocol.PeerGroupAdvertisement;

/**
 * PeerStatusService is an implementation of PeerStatusServiceProvider
 * 
 * @author r.vashira
 * @see PeerStatusServiceProvider
 */
public class PeerStatusService implements PeerStatusServiceProvider {
	/** Constants **/
	protected static final String PING_TOKEN = "PING_TOKEN";
	
	/** Member Variables **/
	private JxtaConnection connection;
	private PeerStatusSender sender;
	private PeerStatusReciever reciever;
	private PeerInfo myInfo;
	
	/**
     * The table of peer status listeners.
     */
    private Set<PeerStatusListener> listeners = new HashSet<PeerStatusListener>();
	
	/**
	 * Default Constructor
	 * @param myPeerName
	 */
	public PeerStatusService(JxtaConnection connection) {
		super();
		
		this.connection = connection;
		this.sender = new PeerStatusSender(this.connection);
		this.reciever = new PeerStatusReciever(this.connection);
	}
	
	/** Public Methods **/
	public PeerInfo getMyInfo() {
		return myInfo;
	}
	
	/**
	 * {@inheritDoc} 
	 * @return
	 */
	public Set<PeerInfo> getPeerCollection() {
		return reciever.getPeerCollection();
	}

	/**
	 * {@inheritDoc} 
	 */
	public void start() {

		// Registering receiver as a listen to DiscoveryService
		connection.addDiscoveryServiceListener(reciever);
		
		// Spawn new thread
		Thread runThread = new Thread((Runnable)reciever);
		runThread.start();
	}

	/**
	 * {@inheritDoc} 
	 */
	public void stop() {
		// Should we release resources holding?
	}

	/*** Interface Methods ***/
	/**
	 * {@inheritDoc} 
	 */
	public synchronized void addListener(PeerStatusListener listener) {
		listeners.add(listener);
	}

	/**
	 * {@inheritDoc} 
	 */
	public boolean removeListener(PeerStatusListener listener) {
		return listeners.remove(listener);
	}
	
	/**
	 * {@inheritDoc} 
	 */
	public boolean isNameExisted(String clientName) {
		// TODO: use sender for broadcasting this name to all peer. When the receiver discover 
		// this adv, it compare with it's clientName.
		// If compare TRUE, it connect with the sender directly and send Message indicate this name cannot be used		
		return false;
	}
	
	/**
	 * {@inheritDoc} 
	 */
	public PeerInfo login(String peerName) {
		// TODO: Actually verify is given peerName is duplicated 
		// by broadcast this name to all peer, if any one see the name is similar to self
		// then reply with reject. Otherwise polling for XX minutes then return PeerInfo 
		
		PeerInfo myInfo = new PeerInfo(peerName);
		myInfo.setId(connection.getMyPeerID());
		
		// side effect: set info to current client
		this.myInfo = myInfo;
		
		// TODO: use sender to send custom message with PeerInfo
		this.sender.sendPingMessage();
		
		return myInfo;
	}


	/**** Anonymous Class ****/
	
	/**
	 * Peer Status Sender which send update status to PeerStatusReciever
	 * 
	 * @author r.vashira
	 * @see PeerStatusReciever
	 */
	protected class PeerStatusSender {
		private JxtaConnection connection;
		
		protected PeerStatusSender(JxtaConnection connection) {
			this.connection = connection;
		}
		
		public void sendPingMessage() {
			// TODO: make it configurable
			long lifetime = 60 * 2 * 1000L;
	        long expiration = 60 * 2 * 1000L;
	        
			// Inform other peer that i'm still alive
        	PeerAdvertisement pingMsg = createPeerPingMessage();
        	DiscoveryService ds = this.connection.getDiscoveryService();
        	
        	// publish the advertisement with a lifetime of 2 minutes
        	System.out.println(
        			"Publishing the ping advertisement with lifetime :" + lifetime + " expiration :" + expiration);
        	//System.out.println(pingMsg.toString());
        	try {
        		ds.publish(pingMsg, lifetime, expiration);
        	} catch (IOException e) {
        		e.printStackTrace();
        	}
        	ds.remotePublish(pingMsg, expiration);
        	// TODO: Logging
        	System.out.println("Sending a Discovery Message");
		}
		
		/** Helper Methods **/
		
		/**
		 * Create a message for ping all peer in Fyta's Realm Group 
		 * @return
		 */
		private PeerAdvertisement createPeerPingMessage() {
			PeerAdvertisement adv =
				(PeerAdvertisement) AdvertisementFactory.newAdvertisement(PeerAdvertisement.getAdvertisementType());

			adv.setPeerGroupID(connection.getMyPeerGroupID());
			adv.setPeerID(connection.getMyPeerID());
			adv.setName(PING_TOKEN);
			return adv;
		}
		
		private PeerAdvertisement createVerifyNameExistedMessage() {
			PeerAdvertisement adv =
				(PeerAdvertisement) AdvertisementFactory.newAdvertisement(PeerAdvertisement.getAdvertisementType());
			
			//TODO: impl
			
			return adv;
		}
	}

	/**
	 * Peer Status Receiver which listen to PeerStatusSender broadcast message
	 * across Fyta's Realm Peer Group. This class run as background service
	 * <p/>
	 * TODO: Using Observer Pattern with PeerStatusListener
	 * 
	 * @author r.vashira
	 * @see PeerStatusSender
	 */
	protected class PeerStatusReciever implements DiscoveryListener, Runnable {
		private Set<PeerInfo> peerCollection;
		private JxtaConnection connection;
		
		/**
		 * @return the peerCollection
		 */
		public Set<PeerInfo> getPeerCollection() {
			return peerCollection;
		}

		protected PeerStatusReciever(JxtaConnection connection) {
			this.peerCollection = new HashSet<PeerInfo>();
			this.connection = connection;
		}
		
		/**
		 * Listening broadcast message Name="PING_TOKEN" every specified wait time 
		 * Note: current wait time is hard code as 60 seconds
		 * 
		 * @see java.lang.Runnable#run()
		 */
		public void run() {
			// TODO: make it configurable
	        long waittime = 20 * 1000L;
	        
	        // Run as bg thread
	        while(true) {
	        	try {
	        		System.out.println("Sleeping for :" + waittime);
	        		Thread.sleep(waittime);
	        	} catch (Exception e) {
	        		// ignored
	        	}

	        	DiscoveryService ds = this.connection.getDiscoveryService();
	        	// Listen Group Adv
	        	// discoveryService.getRemoteAdvertisements(null, DiscoveryService.GROUP, "Name", FYTAS_REALM_GROUP, 1, null);
	        	
	        	// Receive PeerAdvertisement which Name=PING_TOKEN
	        	ds.getRemoteAdvertisements(null, DiscoveryService.PEER, "Name", PING_TOKEN, 1, null);
	        }
		}

		/* (non-Javadoc)
		 * @see net.jxta.discovery.DiscoveryListener#discoveryEvent(net.jxta.discovery.DiscoveryEvent)
		 */
		public void discoveryEvent(DiscoveryEvent event) {
			DiscoveryResponseMsg res = event.getResponse();
			// IMPORTATANT NOTE: this method will be call ONCE a peer publish advertisement
			// TODO: Log
	        System.out.println(" [  Got a Discovery Response [" + res.getResponseCount() + " elements]  from peer : " + event.getSource() + "  ]");
	        //Tools.popInformationMsg(JxtaConnection.FYTAS_REALM_GROUP, "[  Got a Discovery Response [" + res.getResponseCount() + " elements]  from peer : " + event.getSource() + "  ]");
	        
	        recievePingFromPeer(event);
		}
		
		/** Helper Methods **/
		private void recievePingFromPeer(DiscoveryEvent ev) {
			DiscoveryResponseMsg res = ev.getResponse();
	    	Advertisement adv;
	        Enumeration<Advertisement> en = res.getAdvertisements();

	        if (en != null) {
	        	System.out.println("In PeerStatusService.recievePingFromPeer() get an Adv");
	            while (en.hasMoreElements()) {
	                adv = en.nextElement();
	                //System.out.println("ADV-TYPE: " + adv.getAdvType());
	                // TODO: Move to log
	                System.out.println("Found new PeerAdvertisement");
	                //System.out.println(adv);
	                if(adv instanceof PeerAdvertisement) {
	                	
	                	// Filter system generated PeerAdvertisement ou
	                	//if(isSystemGeneratedPeerAdv((PeerAdvertisement)adv))
	                	//	return;
	                	
	            		managePeerCollection((PeerAdvertisement)adv);
	            	}
	            }
	        }
			
	    }
	
		private void managePeerCollection(PeerAdvertisement peerAdv) {
			PeerID sourceId = peerAdv.getPeerID();
			String peerName = peerAdv.getName();

			// Create PeerInfo then add to peerCollection
			PeerInfo newPeer = new PeerInfo(peerName);
			newPeer.setId(sourceId);

			// FIXME: now peerCollection include self
			boolean addSuccess = this.peerCollection.add(newPeer);
			if(addSuccess) {
				System.out.println("Add new Peer to the collection");
				
				// [event] newPeerConnected
				PeerStatusEvent event = new PeerStatusEvent(this, newPeer);
				PeerStatusListener[] allListeners = listeners.toArray(new PeerStatusListener[0]);
				for (PeerStatusListener listener : allListeners) {
					listener.newPeerConnected(event);
				}
			} 
			/*else {
				System.out.println("Peer is already in the collection");
			}*/
		}
	
		/** @deprecated */
		private boolean isSystemGeneratedPeerAdv(PeerAdvertisement adv) {
			return "FytasRealm".equals(adv.getName());
		}
	}
}
